
package com.mephi.io;


import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.log4j.Logger;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.mephi.MephistoChess;
import com.mephi.controller.GameImpl;
import com.mephi.controller.GameResult;
import com.mephi.controller.PositionImpl;
import com.mephi.exception.InvalidGameFormatException;
import com.mephi.interfaces.Game;
import com.mephi.interfaces.LoadSaveGame;
import com.mephi.interfaces.Position;
import com.mephi.model.Move;
import com.mephi.util.AuxClass;
import com.mephi.util.Constants;
import com.mephi.util.Ext;
import com.mephi.util.PieceLoaderKey;
import com.mephi.util.PieceLoaderKeyMap;


/**
 * Default implementation of saving game in xml format.
 * @author mephisto
 * 2011-07-23
 */
public class LoadSaveGameImpl implements LoadSaveGame<Move>
{
	/**
	 * 
	 * @param path the path to a file to set
	 * @return the loaded game
	 * 
	 * @throws NullPointerException if path is null
	 */
	@Override
	public Game<Move> loadGame(final String path)
	{
		AuxClass.checkArgument(path, new NullPointerException("path is null!"));
		
		int dotPos = path.lastIndexOf(Constants.DOT_STRING);
		String extension = path.substring(dotPos+1);
		
		Game<Move> game = new GameImpl<Move>();
		if (isExtSupported(extension))
		{
		    SwingWorker<Game<Move>, String> worker = new SwingWorkerLoader(path);
	        worker.execute();
		         
	        try
			{
				game = worker.get();
			}
			catch (InterruptedException e)
			{
				logger.debug(e);
				JOptionPane.showMessageDialog(MephistoChess.gui.getFraMainFrame(),
						e.getMessage(), "Problem with saving!", JOptionPane.ERROR_MESSAGE);
			}
			catch (ExecutionException e)
			{
				logger.debug(e);
				JOptionPane.showMessageDialog(MephistoChess.gui.getFraMainFrame(),
						e.getMessage(), "Problem with saving!", JOptionPane.ERROR_MESSAGE);
			}	
		}
		return game;
	}

	/**
	 * 
	 * @param path the path to a file to set
	 * 
	 * @throws NullPointerException if doc or path is/are null
	 */
	@Override
	public void saveGame(Document doc, String path)
	{
		AuxClass.checkArgument(doc, new NullPointerException("doc is null!"));
		AuxClass.checkArgument(path, new NullPointerException("path is null!"));	
		// Prepare the DOM document for writing
	    Source source = new DOMSource(doc);
		// Prepare the output file
	    if (!path.endsWith("."+Ext.xml.toString()))
	    	path += ".xml";
	    File file = new File(path);
	    Result result = new StreamResult(file);

		try
		{
			// Write the DOM document to the file
			Transformer transformer = TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		    //transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
		    //System.setProperty("javax.xml.transform.TransformerFactory", "org.apache.xalan.xsltc.trax.TransformerFactoryImpl");
		    transformer.transform(source, result);
		    logger.info("The game was saved " + path);
		}
		catch (TransformerConfigurationException e) 
		{
			logger.error(e);
			JOptionPane.showMessageDialog(MephistoChess.gui.getFraMainFrame(),
					e.getMessage(), "Problem with saving", JOptionPane.ERROR_MESSAGE);
		} 
		catch (TransformerException e)
		{
			logger.error(e);
			JOptionPane.showMessageDialog(MephistoChess.gui.getFraMainFrame(),
					e.getMessage(), "Problem with saving", JOptionPane.ERROR_MESSAGE);
		}
	}

	private boolean isExtSupported(String ext)
	{   
		for (Ext it : supportedExtentions)
			if (it.toString().equals(ext))
				return true;
		return false;
	}
	
	private static final List<Ext> supportedExtentions = new ArrayList<Ext>(); 
	static
	{
		supportedExtentions.add(Ext.xml);
	}

	private DocumentBuilder builder;
	private Logger logger = Logger.getLogger("LOG");
	
	
	final class SwingWorkerLoader extends SwingWorker<Game<Move>, String>
	{
		public SwingWorkerLoader(String path)
		{
			this.path = path;
		}
		
		@Override
        protected Game<Move> doInBackground() throws Exception
        {
           if (builder == null)
           {
              DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
              builder = factory.newDocumentBuilder();
             /* javax.xml.parsers.DocumentBuilderFactory A;
              com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl b;*/
           }
           
           Game<Move> game = new GameImpl<Move>();
           try
           {
        	   Document doc = builder.parse(path); 
               Element rootProfileNode = doc.getDocumentElement();
               logger.debug(rootProfileNode.getTagName() + " was read");                   
               NodeList gamesNodeList = rootProfileNode.getElementsByTagName("games");
              
              //reading "<games>" - only not empty Elements(ignoring spaces)
              for(int i=0, size = gamesNodeList.getLength(); i<size; i++)
              {
             	 Node fullGameNodeList = gamesNodeList.item(i);
             	 
             	 if (fullGameNodeList instanceof Element)
             	 {
             		 Element gamesElem = (Element)fullGameNodeList;
             		 logger.debug("Reading " + gamesElem.getTagName());
             		 
             		 NodeList gameNodeList =  fullGameNodeList.getChildNodes();
             		 
             		//reading "<game>" - only not empty Elements(ignoring spaces)
             		 for(int j=0, gamesSize = gameNodeList.getLength(); j<gamesSize; j++)
                     {
                     	 //logger.debug("Inside loop");
                     	 Node gameNode = gameNodeList.item(j);
                     	 
                     	 //it causes omitting empty nodes(spaces)
                     	 if (gameNode instanceof Element)
                     	 {
                         	fillMainDataAboutGame(gameNode, game);
                         	
                         	NodeList movesPosListNodeList =  gameNode.getChildNodes();
                         	Position position = null;
                         	
                         	for(int k=0, movesPosListSize = movesPosListNodeList.getLength(); k<movesPosListSize; k++)
                         	{
                         		Node movePosListNode = movesPosListNodeList.item(k);
                         		//logger.debug("Reading node: " + movePosListNode.getNodeName());
                         		
                            	 //it causes omitting empty nodes(spaces)
                            	 if (movePosListNode instanceof Element)
                            	 {
                            		//<position>
                            		 if (movePosListNode.getNodeName().equals("position"))
                            		 {
                            			 NodeList positionNodeList =  movePosListNode.getChildNodes();	 
                            			 Map<String, PieceLoaderKey> whitePositionMap = null;
                            			 Map<String, PieceLoaderKey> blackPositionMap = null;
                            			 for(int m=0, posSize = positionNodeList.getLength(); m<posSize; m++)
                            			 {
                            				 Node posNode = positionNodeList.item(m);
                            				//it causes omitting empty nodes(spaces)
	                                     	 if (posNode instanceof Element)
	                                     	 {
	                                     		 Element posElem = (Element)posNode;
	                                     		 if (posElem.getNodeName().equals("white"))
	                                     			whitePositionMap = loadPosition(posElem, 
	                                     					PieceLoaderKeyMap.INSTANCE.getWhitePiecesMap());
	                                     		 else if (posNode.getNodeName().equals("black"))
	                                     			blackPositionMap = loadPosition(posElem, 
	                                     					PieceLoaderKeyMap.INSTANCE.getBlackPiecesMap());
	                                     	 }
                            			 }
                            			 position = new PositionImpl(whitePositionMap, blackPositionMap);
                            		 }
                            		 else //no position node - it is starting position
                            		 {
                            			 //position = new PositionImpl();
                            		 }
                            		 
                            		 //<movesList>
                            		 if (movePosListNode.getNodeName().equals("movesList"))     	 
	                            		fillMoveList(movePosListNode.getChildNodes(), game);	 
                            	 } 
                         	 }
                         	 game.setPosition(position);
                     	 } 
                      }
             	 }
              }
           }
           catch (Exception e)
           {
        	   logger.error(e);
        	   JOptionPane.showMessageDialog(MephistoChess.gui.getFraMainFrame(),
   					e.getMessage(), "Problem with saving!", JOptionPane.ERROR_MESSAGE);
           }
           return game;
        }

		@Override
        protected void done() {}
        
		private void fillMainDataAboutGame(Node gameNode, Game<Move> game) 
				throws InvalidGameFormatException
		{
			Element gameElem = (Element)gameNode;
    		logger.debug("Reading " + gameElem.getTagName());	 
    		NamedNodeMap nodeMap = gameElem.getAttributes();
    		 
    		//getting date and time
        	String[] strDate = nodeMap.getNamedItem(
        			 "date").getTextContent().split("\\.");
			if (strDate.length != 3)
				throw new InvalidGameFormatException(
						"Invalid game format causes errors during loading! Wrong date!");
			String[] strTime = nodeMap.getNamedItem(
					"time").getTextContent().split(Constants.COLON_STRING);

			if (strTime.length != 3)
				throw new InvalidGameFormatException(
						"Invalid game format causes errors during loading! Wrong time!");
    	    Calendar calendar = new GregorianCalendar(
    					Integer.parseInt(strDate[0]),
    					Integer.parseInt(strDate[1])-1,
    					Integer.parseInt(strDate[2]),
    					Integer.parseInt(strTime[0]),
    					Integer.parseInt(strTime[1]),
    					Integer.parseInt(strTime[2])
    					);
    	     
    		game.setCalendar(calendar);
        	game.setEvent(nodeMap.getNamedItem("event").getTextContent());
        	game.setSite(nodeMap.getNamedItem("site").getTextContent());
        	game.setRound(Integer.parseInt(
        			nodeMap.getNamedItem("round").getTextContent()));
        	game.setWhite(nodeMap.getNamedItem("white").getTextContent());
        	game.setBlack(nodeMap.getNamedItem("black").getTextContent());
        	game.setResult(GameResult.newInstance(
        			nodeMap.getNamedItem("result").getTextContent()));
        	game.setTimeControl(nodeMap.getNamedItem("timeControl").getTextContent()); 
        	game.setWhiteElo(Integer.parseInt(
        			nodeMap.getNamedItem("whiteElo").getTextContent()));
        	game.setBlackElo(Integer.parseInt(
        			nodeMap.getNamedItem("blackElo").getTextContent()));
		}
		
		private void fillMoveList(NodeList movesNodeList, Game<Move> game) 
				throws InvalidGameFormatException, DOMException
		{
			//<move>
          	for(int m=0, movesSize = movesNodeList.getLength(); m<movesSize; m++)
          	{
          		Node moveNode = movesNodeList.item(m);

             	//it causes omitting empty nodes(spaces)
             	if (moveNode instanceof Element)
             	{
             		Element moveElem = (Element)moveNode;
             		 
             		String[] movTab = moveElem.getTextContent().split(Constants.SPACE_STRING);
             		if (movTab.length != 2)
          				throw new InvalidGameFormatException(
          						"Invalid game format causes errors during loading! Wrong move!- " 
          						+ moveElem.getTextContent());
             		Move move = new Move(movTab[0], movTab[1]);
             		game.add(move);
             	}
          	}
		}
		
		//loads position for white or black if you give the proper node <white> or <black>
		//under <position>
		private Map<String, PieceLoaderKey> loadPosition(Element posElem, Map<String, PieceLoaderKey> piecesMap)
		{
			AuxClass.checkArgument(posElem, new NullPointerException("posElem cannot be null!"));
			AuxClass.checkArgument(piecesMap, new NullPointerException("piecesMap cannot be null!"));
			logger.debug("Reading " + posElem.getTagName());
			NamedNodeMap posMap = posElem.getAttributes();
			String kingStr = posMap.getNamedItem(Constants.KING).getTextContent();
			if (kingStr.split(Constants.COMMA_STRING).length > 1)
				throw new IllegalArgumentException("Two kings on the chessboard! - " + kingStr);
			StringBuilder sBAux = new StringBuilder(Constants.COLON_STRING).append(Constants.SPACE_STRING);
			logger.debug(Constants.KING + sBAux + kingStr);
			Map<String, PieceLoaderKey> positionMap = new HashMap<String, PieceLoaderKey>();
			positionMap.put(kingStr, piecesMap.get(Constants.KING));
			
			logger.debug(Constants.QUEENS + sBAux);
			String queenStr = posMap.getNamedItem(Constants.QUEENS).getTextContent();
			if (queenStr.split(Constants.COMMA_STRING).length > 9)
				throw new IllegalArgumentException("Too many queens on the chessboard! - " + queenStr);
			for(String it : queenStr.split(Constants.COMMA_STRING))
			{
				positionMap.put(it.trim(), piecesMap.get(Constants.QUEENS));
				logger.debug(it);
			}
			
			logger.debug(Constants.ROOKS + sBAux);		 
			String rookStr = posMap.getNamedItem(Constants.ROOKS).getTextContent();
			if (rookStr.split(Constants.COMMA_STRING).length > 10)
				throw new IllegalArgumentException("Too many rooks on the chessboard! - " + rookStr);
			for(String it : rookStr.split(Constants.COMMA_STRING))
			{
				positionMap.put(it.trim(), piecesMap.get(Constants.ROOKS));	
				logger.debug(it);
			}
				
			logger.debug(Constants.BISHOPS + sBAux);
			String bishopStr = posMap.getNamedItem(Constants.BISHOPS).getTextContent();
			if (bishopStr.split(Constants.COMMA_STRING).length > 10)
				throw new IllegalArgumentException("Too many bishops on the chessboard! - " + bishopStr);		
			for(String it : bishopStr.split(Constants.COMMA_STRING))
			{
				positionMap.put(it.trim(), piecesMap.get(Constants.BISHOPS));
				logger.debug(it);
			}	
				
			logger.debug(Constants.KNIGHTS + sBAux);
			String knightStr = posMap.getNamedItem(Constants.KNIGHTS).getTextContent();
			if (knightStr.split(",").length > 10)
				throw new IllegalArgumentException("Too many knights on the chessboard! - " + knightStr);
			for(String it : knightStr.split(Constants.COMMA_STRING))
			{
				positionMap.put(it.trim(), piecesMap.get(Constants.KNIGHTS));
				logger.debug(it);
			}		
				
			logger.debug(Constants.PAWNS + sBAux);	
			String pawnStr = posMap.getNamedItem(Constants.PAWNS).getTextContent();
			if (pawnStr.split(Constants.COMMA_STRING).length > 8)
				throw new IllegalArgumentException("Too many pawns on the chessboard! - " + pawnStr);
			for(String it : pawnStr.split(Constants.COMMA_STRING))
			{
				positionMap.put(it.trim(), piecesMap.get(Constants.PAWNS));	
				logger.debug(it);
			}			
					
			return positionMap;	 
		}
		
		
        private Logger logger = Logger.getLogger("LOG");
		private String path = null;
     }
}
